package org.skatiger.jrove.spring.security;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.skatiger.jrove.JroveException;
import org.skatiger.jrove.component.UIComponentBase;
import org.skatiger.jrove.spring.utils.JroveApplicationContextUtil;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParseException;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.expression.WebSecurityExpressionHandler;

public class Authorize extends UIComponentBase {
    //~ Instance fields ================================================================================================

    private String ifAllGranted = "";
    private String ifAnyGranted = "";
    private String ifNotGranted = "";
    private String access;
    private String url;
    private String method;

    //~ bean mehtod
    public String getIfAllGranted() {
        return getPropertyValue("ifAllGranted", ifAllGranted);
    }

    public String getIfAnyGranted() {
        return getPropertyValue("ifAnyGranted", ifAnyGranted);
    }

    public String getIfNotGranted() {
        return getPropertyValue("ifNotGranted", ifNotGranted);
    }

    public void setIfAllGranted(String ifAllGranted) {
        this.ifAllGranted = ifAllGranted;
    }

    public void setIfAnyGranted(String ifAnyGranted) {
        this.ifAnyGranted = ifAnyGranted;
    }

    public void setIfNotGranted(String ifNotGranted) {
        this.ifNotGranted = ifNotGranted;
    }

    public void setAccess(String access) {
        this.access = access;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    public String getAccess() {
        return getPropertyValue("access", access);
    }

    public String getUrl() {
        return getPropertyValue("url", url);
    }

    public String getMethod() {
        return getPropertyValue("method", method);
    }

    //~ Methods ========================================================================================================

    private Set<String> authoritiesToRoles(Collection<GrantedAuthority> c) {
        Set<String> target = new HashSet<String>();
        for (GrantedAuthority authority : c) {
            if (null == authority.getAuthority()) {
                throw new IllegalArgumentException(
                        "Cannot process GrantedAuthority objects which return null from getAuthority() - attempting to process "
                                + authority.toString());
            }

            target.add(authority.getAuthority());
        }

        return target;
    }

    @Override
    public boolean isRendered() {
        return super.isRendered() ? _authorize() : false;
    }

    private boolean _authorize() {
        Authentication currentUser = SecurityContextHolder.getContext()
                .getAuthentication();

        if (currentUser == null) {
            return false;
        }

        if (access != null && access.length() > 0) {
            return authorizeUsingAccessExpression(currentUser);
        } else if (url != null && url.length() > 0) {
            return authorizeUsingUrlCheck(currentUser);
        }
        return __authorize();
    }

    private boolean __authorize() {
        if (((null == ifAllGranted) || "".equals(ifAllGranted))
                && ((null == ifAnyGranted) || "".equals(ifAnyGranted))
                && ((null == ifNotGranted) || "".equals(ifNotGranted))) {
            return false;
        }

        final Collection<GrantedAuthority> granted = getPrincipalAuthorities();

        final String evaledIfNotGranted = getIfNotGranted();

        if ((null != evaledIfNotGranted) && !"".equals(evaledIfNotGranted)) {
            Set<GrantedAuthority> grantedCopy = retainAll(granted,
                    parseAuthoritiesString(evaledIfNotGranted));

            if (!grantedCopy.isEmpty()) {
                return false;
            }
        }

        final String evaledIfAllGranted = getIfAllGranted();

        if ((null != evaledIfAllGranted) && !"".equals(evaledIfAllGranted)) {
            if (!granted
                    .containsAll(parseAuthoritiesString(evaledIfAllGranted))) {
                return false;
            }
        }

        final String evaledIfAnyGranted = getIfAnyGranted();

        if ((null != evaledIfAnyGranted) && !"".equals(evaledIfAnyGranted)) {
            Set<GrantedAuthority> grantedCopy = retainAll(granted,
                    parseAuthoritiesString(evaledIfAnyGranted));

            if (grantedCopy.isEmpty()) {
                return false;
            }
        }

        return true;
    }

    private Collection<GrantedAuthority> getPrincipalAuthorities() {
        Authentication currentUser = SecurityContextHolder.getContext()
                .getAuthentication();

        if (null == currentUser) {
            return Collections.emptyList();
        }

        return currentUser.getAuthorities();
    }

    private Set<GrantedAuthority> parseAuthoritiesString(
            String authorizationsString) {
        final Set<GrantedAuthority> requiredAuthorities = new HashSet<GrantedAuthority>();
        requiredAuthorities.addAll(AuthorityUtils
                .commaSeparatedStringToAuthorityList(authorizationsString));

        return requiredAuthorities;
    }

    /**
     * Find the common authorities between the current authentication's {@link GrantedAuthority} and the ones
     * that have been specified in the tag's ifAny, ifNot or ifAllGranted attributes.<p>We need to manually
     * iterate over both collections, because the granted authorities might not implement {@link
     * Object#equals(Object)} and {@link Object#hashCode()} in the same way as {@link GrantedAuthorityImpl}, thereby
     * invalidating {@link Collection#retainAll(java.util.Collection)} results.</p>
     * <p>
     * <strong>CAVEAT</strong>:  This method <strong>will not</strong> work if the granted authorities
     * returns a <code>null</code> string as the return value of {@link GrantedAuthority#getAuthority()}.
     * </p>
     *
     * @param granted The authorities granted by the authentication. May be any implementation of {@link
     *        GrantedAuthority} that does <strong>not</strong> return <code>null</code> from {@link
     *        GrantedAuthority#getAuthority()}.
     * @param required A {@link Set} of {@link GrantedAuthorityImpl}s that have been built using ifAny, ifAll or
     *        ifNotGranted.
     *
     * @return A set containing only the common authorities between <var>granted</var> and <var>required</var>.
     *
     */
    private Set<GrantedAuthority> retainAll(
            final Collection<GrantedAuthority> granted,
            final Set<GrantedAuthority> required) {
        Set<String> grantedRoles = authoritiesToRoles(granted);
        Set<String> requiredRoles = authoritiesToRoles(required);
        grantedRoles.retainAll(requiredRoles);

        return rolesToAuthorities(grantedRoles, granted);
    }

    private Set<GrantedAuthority> rolesToAuthorities(Set<String> grantedRoles,
            Collection<GrantedAuthority> granted) {
        Set<GrantedAuthority> target = new HashSet<GrantedAuthority>();

        for (String role : grantedRoles) {
            for (GrantedAuthority authority : granted) {
                if (authority.getAuthority().equals(role)) {
                    target.add(authority);
                    break;
                }
            }
        }

        return target;
    }

    private boolean authorizeUsingAccessExpression(Authentication currentUser) {
        // Get web expression
        WebSecurityExpressionHandler handler = getExpressionHandler();

        Expression accessExpression;
        try {
            accessExpression = handler.getExpressionParser().parseExpression(
                    access);

        } catch (ParseException e) {
            throw new JroveException(e);
        }
        ServletRequest request = (ServletRequest) getViewContext()
                .getExternalContext().getRequest();
        ServletResponse response = (ServletResponse) getViewContext()
                .getExternalContext().getResponse();
        FilterInvocation f = new FilterInvocation(request, response,
                DUMMY_CHAIN);

        if (ExpressionUtils.evaluateAsBoolean(accessExpression,
                handler.createEvaluationContext(currentUser, f))) {
            return true;
        }

        return false;
    }

    private boolean authorizeUsingUrlCheck(Authentication currentUser) {
        return getPrivilegeEvaluator().isAllowed(
                getViewContext().getExternalContext().getRequestContextPath(),
                url, method, currentUser);
    }

    WebSecurityExpressionHandler getExpressionHandler() {

        ApplicationContext ctx = JroveApplicationContextUtil
                .findApplicationContext(getViewContext());
        Map<String, WebSecurityExpressionHandler> expressionHdlrs = ctx
                .getBeansOfType(WebSecurityExpressionHandler.class);

        if (expressionHdlrs.size() == 0) {
            throw new JroveException(
                    "No visible WebSecurityExpressionHandler instance could be found in the application "
                            + "context. There must be at least one in order to support expressions in JSP 'authorize' tags.");
        }

        return (WebSecurityExpressionHandler) expressionHdlrs.values()
                .toArray()[0];
    }

    WebInvocationPrivilegeEvaluator getPrivilegeEvaluator() {

        ApplicationContext ctx = JroveApplicationContextUtil
                .findApplicationContext(getViewContext());
        Map<String, WebInvocationPrivilegeEvaluator> wipes = ctx
                .getBeansOfType(WebInvocationPrivilegeEvaluator.class);

        if (wipes.size() == 0) {
            throw new JroveException(
                    "No visible WebInvocationPrivilegeEvaluator instance could be found in the application "
                            + "context. There must be at least one in order to support the use of URL access checks in 'authorize' tags.");
        }

        return (WebInvocationPrivilegeEvaluator) wipes.values().toArray()[0];
    }

    private static final FilterChain DUMMY_CHAIN = new FilterChain() {
        public void doFilter(ServletRequest request, ServletResponse response)
                throws IOException, ServletException {
            throw new UnsupportedOperationException();
        }
    };
}
